enforce a daily download limit#2160
Conversation
b90fe09 to
564851a
Compare
There was a problem hiding this comment.
Pull request overview
Implements a Redis-backed per-user daily download limit to mitigate abusive “download everything” behavior, including UI hiding of download links and server-side enforcement with HTTP 429 responses.
Changes:
- Add
utils.download_limithelpers to count daily downloads (UTC day) in Redis and deduplicate multi-request downloads via a short-lived sentinel. - Enforce the limit in sound, pack, collection, and bookmark-category download endpoints (429) and hide/replace download affordances in templates (modal + disabled-looking UI).
- Add Redis-requiring tests plus CI/test-environment updates (Redis service, settings, pytest marker).
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| utils/tests/test_download_limit.py | Unit tests for Redis daily counter behavior and fail-open semantics. |
| utils/download_limit.py | Core daily counter + “in progress” sentinel logic and 429 response helper. |
| templates/sounds/sound.html | Hide direct download link when over limit; show modal trigger instead. |
| templates/sounds/pack.html | Same as above for pack downloads. |
| templates/sounds/download_limit_reached.html | New 429 page template. |
| templates/molecules/modal_download_limit.html | New modal template explaining the limit. |
| templates/collections/your_collections.html | Pass limit state into collection card rendering. |
| templates/collections/display_collection.html | Hide collection download link on cards when over limit. |
| templates/collections/collection.html | Disable collection download button and show modal trigger when over limit. |
| templates/bookmarks/bookmarks.html | Disable bookmark-category download icon and show modal trigger when over limit. |
| sounds/views.py | Apply guard + dedup logic for sound/pack downloads; add modal view; pass context flag. |
| sounds/tests/test_sound.py | Ensure on-commit callbacks execute so sentinel/count logic works in tests. |
| sounds/tests/test_download_limit_views.py | End-to-end tests for 429 behavior and link hiding across pages. |
| pyproject.toml | Register redis pytest marker. |
| fscollections/views.py | Apply guard + dedup logic for collection downloads; pass context flag. |
| fscollections/templatetags/display_collections.py | Accept download_limit_reached input and forward to template. |
| freesound/urls.py | Add route for the download-limit modal. |
| freesound/test_settings.py | Add Redis-backed abuse cache for tests. |
| freesound/settings.py | Add abuse cache + default daily limit settings/message. |
| docker-compose.test.yml | Add Redis service for test stack. |
| bookmarks/views.py | Apply guard + dedup logic for bookmark-category downloads; pass context flag. |
| .github/workflows/unit-tests.yml | Pull Redis image in CI. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {% if download_limit_reached %} | ||
| <a class="no-hover btn-primary display-inline-block w-100 text-center opacity-050 cursor-pointer" data-toggle="modal-default" data-modal-content-url="{% url 'download-limit-modal' %}?ajax=1" title="Daily download limit reached">Download pack</a> | ||
| {% else %} |
ffont
left a comment
There was a problem hiding this comment.
New logic looks much cleaner in code. I added a couple of relevant comments though :)
|
|
||
|
|
||
| def download_limit_reached(user_id): | ||
| return get_daily_download_count(user_id) >= settings.MAX_DOWNLOADS_PER_DAY |
There was a problem hiding this comment.
Shouldn't we skip download limits for sound uploaders and possibly some other cases? Although you already mentioned the threshold is set well below any "human" download activity
There was a problem hiding this comment.
I'd probably suggest that we leave this one for now and add another issue for it, because as we said it should never happen (99% of downloads in the last year were < 50/user/month and our limit is 200/day)
9aacf67 to
d9e4e15
Compare
This link should open the modal, not trigger a download
Count the number of downloads that a user makes over a day. Align all users to UTC for simplicity. Use Redis to store the count. Count Sounds, Packs, Collections, and Bookmark Category downloads as 1 download each time. Once a user goes over the limit, don't show a download link, and prevent the direct download link from providing the file (HTTP 429). Set an "in progress" download key when the user starts a download, and don't count again if they access the URL when the download sentinel is active - this prevents download managers that request different ranges from counting multiple times. Add the same guard to Collection downloads (it was missing)
d9e4e15 to
8074fe9
Compare
Description
Very infrequently we see people try to download all of Freesound via the download buttons on each sound. Stop this from happening.
Count the number of downloads that a user makes over a day. Align all users to UTC for simplicity. Use Redis to store the count. Count Sounds, Packs, Collections, and Bookmark Category downloads as 1 download each time.
Once a user goes over the limit, don't show a download link, and prevent the direct download link from providing the file (HTTP 429).
Set an "in progress" download key when the user starts a download, and don't count again if they access the URL when the download sentinel is active - this prevents download managers that request different ranges from counting multiple times.
Add the same guard to Collection downloads (it was missing)
The In progress download key in redis was previously set, but we also checked the http Range header to see if this was a "new" or "continued" download. Remove the Range header entirely, because the redis check performs pretty much the same check and we were using it anyway.
There was one bug where a user could maliciously request
Range: 0-in order to circumvent the check, although we expect that this probably never happened.The new check does mean that if there is a large file (takes longer than 5 minutes to download) and the client makes multiple requests to download it, that we'll count this twice. We'll check this in a few weeks but don't expect that it'll cause an issue either.
Deployment steps:
Requires the "abuse" cache to be added in prod. We'll add it to volatile.